package com.fenqile.pop;

import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.charset.Charset;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;
import java.util.TreeMap;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.apache.http.HttpEntity;
import org.apache.http.HttpHeaders;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicHeader;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;

/**
 * 分期乐客户端，分期乐的接口调用从这里开始
 * 
 * ThreadSafe
 * 
 * @author Coby [cobylin@live.com] 2017
 */
public class FenQiLeApiClient {
	public static final Logger logger = LoggerFactory.getLogger(FenQiLeApiClient.class);
	public static final ParameterTypeSerializer PARAMETER_TYPE_SERIALIZER = new ParameterTypeSerializer(); 
	
	public static final String API_URL = "http://pop.api.fenqile.com/router/rest.json";
	public static final String API_VERSION = "1.4";
	public static final Charset API_CHARSET = Charset.forName("utf-8");
	
	private final HttpClient httpClient;
	private final ObjectMapper objectMapper;
	private final String partnerId;
	private final String partnerKey;
	
	public FenQiLeApiClient(String partnerId, String partnerKey) {
		this.partnerId = partnerId;
		this.partnerKey = partnerKey;
		
		HttpClientBuilder httpClientBuilder = HttpClients.custom();
		httpClientBuilder.disableCookieManagement();
		httpClientBuilder.disableAuthCaching();
		httpClientBuilder.disableContentCompression();
		httpClientBuilder.disableConnectionState();
		httpClientBuilder.setDefaultHeaders(Arrays.asList(
			//必须使用此HTTP头信息，否则服务器会返回签名错误，文档没有提及这部分内容
			new BasicHeader(HttpHeaders.CONTENT_TYPE, "Application/X-WWW-FORM-URLENCODED"),
			new BasicHeader(HttpHeaders.CONNECTION, "Keep-Alive")
		));
		this.httpClient = httpClientBuilder.build();
		this.objectMapper = buildObjectMapper();
	}
	
	public FenQiLeApiClient(String partnerId, String partnerKey, HttpClient httpClient) {
		this.partnerId = partnerId;
		this.partnerKey = partnerKey;
		this.httpClient = httpClient;
		this.objectMapper = buildObjectMapper();
	}
	
	protected ObjectMapper buildObjectMapper() {
		ObjectMapper objectMapper = new ObjectMapper();
		SimpleModule module = new ResultTypeDeserializer();
		objectMapper.registerModule(module);
		objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
		return objectMapper;
	}
	
	public String getPartnerId() {
		return partnerId;
	}
	
	public String getPartnerKey() {
		return partnerKey;
	}
	
	public String request(CommonParameter comm, BusinessParameter<?> parameter) {
		
		String apiMethod = parameter.getApiMethod();
		String signSource, sign, url, body, result;
		
		//签名
		comm.sign = null;
		signSource = genSignSource(comm, parameter);
		sign = md5(signSource);
		comm.sign = sign;
		
		//调用远程接口
		url = API_URL + '?' + toLinkString(toMap(comm, false));
		body = toLinkString(toMap(parameter, true));
		result = sendPost(url, body);
		
		logger.debug("\n" +
				"\tURL\t: {}\n\tMETHOD\t: {}\n\tSIGNS\t: {}\n\tSIGN\t: {}\n\tBODY\t: {}\n\tRESULT\t: {}",
				url, apiMethod, signSource, sign, body, unicodeToString(result));
		
		return result;
		
	}
	
	@SuppressWarnings("unchecked")
	public <T> T request(BusinessParameter<T> parameter) {
		
		CommonParameter comm = genCommonParameter();
		comm.method = parameter.getApiMethod();
		
		String result = request(comm, parameter);
		
		//处理返回类型
		JsonNode resultJson;
		try {
			resultJson = objectMapper.readValue(result, JsonNode.class);
		} catch (Exception e) {
			throw new IllegalArgumentException(e);
		}
	    if (resultJson.size() == 1) {
	    	JsonNode errorJson = resultJson.get("error_response");
		    if (errorJson == null) {
		    	JsonNode dataJson = resultJson.elements().next();
		    	T resultObject;
				try {
					JavaType type = objectMapper.getTypeFactory().constructType(parameter.getResultType());
					resultObject = (T)objectMapper.treeToValue(dataJson, type.getRawClass());
				} catch (JsonProcessingException e) {
					throw new IllegalArgumentException(e);
				}
		    	return resultObject;
		    }
		    else {
		    	int code = errorJson.get("code").asInt();
		    	String msg = errorJson.get("msg").asText();
		    	String sub_msg = errorJson.get("sub_msg").asText();
		    	throw new FenQiLeApiException(code, msg, sub_msg);
		    }
	    }
	    else throw new IllegalStateException("Only one data structure can be returned:\n" + result);
		
	}
	
	protected CommonParameter genCommonParameter() {
		CommonParameter comm = new CommonParameter();
		comm.partner_id = partnerId;
		comm.timestamp = System.currentTimeMillis() / 1000;
		comm.format = "json";
		comm.v = API_VERSION;
		return comm;
	}
	
	public String genSignSource(CommonParameter comm, BusinessParameter<?> bus) {
		
		TreeMap<String, String> allFields = new TreeMap<>();
		
		Map<String, String> commMap = toMap(comm, false);
		allFields.putAll(commMap);
		
		Map<String, String> busMap = toMap(bus, false);
		allFields.putAll(busMap);
		
		String str = toLinkString(allFields);
		str = str + "&partner_key" + '=' + partnerKey;
		
		return str;
	}
	
	public static String md5(String signSource) {
		
		try {
			MessageDigest digester = MessageDigest.getInstance("MD5");
			byte[] sourceBytes = signSource.getBytes(API_CHARSET);
			byte[] digestBytes = digester.digest(sourceBytes);
			//return new BigInteger(1, digestBytes).toString(16);
			return bytesToHex(digestBytes);
		} catch (NoSuchAlgorithmException e) {
			throw new RuntimeException(e);
		}
		
	}
	
	public static Map<String, String> toMap(Object obj, boolean containArray) {
		Map<String, String> map = new HashMap<>();
		Class<?> clazz = obj.getClass();
		Object value = null;
		String valueString = null;
		Field[] fs = clazz.getFields();
		for (Field f : fs) {
			value = null;
			valueString = null;
			if (Modifier.isStatic(f.getModifiers()) || Modifier.isTransient(f.getModifiers())) continue;
			if (!containArray && (f.getType().isArray() || Collection.class.isAssignableFrom(f.getType()))) continue;
			try {
				value = f.get(obj);
				if (value == null) continue;
			} catch (Exception e) {
				throw new IllegalArgumentException(e);
			}
			
			if (value instanceof CharSequence) {
				valueString = value.toString();
			}
			else if ((valueString = PARAMETER_TYPE_SERIALIZER.serialize(value)) == null) {
				valueString = String.valueOf(value);
			}
			map.put(f.getName(), valueString);
		}
		return map;
	}
	
	public static String toLinkString(Map<String, String> paraMap) {
		if (paraMap.isEmpty()) return "";
		StringBuilder sb = new StringBuilder();
		for (Map.Entry<String, String> fc : paraMap.entrySet()) {
			sb.append(fc.getKey()).append('=').append(fc.getValue()).append('&');
		}
		sb.setLength(sb.length() - 1);
		return sb.toString();
	}
	
	public static String bytesToHex(byte[] buf) {  
        StringBuffer sb = new StringBuffer();  
        for (int i = 0; i < buf.length; i++) {  
            String hex = Integer.toHexString(buf[i] & 0xFF);  
            if (hex.length() == 1) {  
                hex = '0' + hex;  
            }  
            sb.append(hex.toLowerCase());  
        }  
        return sb.toString();  
    }  
	
	public static String unicodeToString(String str) {
		if (str == null) return null;
	    Pattern pattern = Pattern.compile("(\\\\u(\\p{XDigit}{4}))");    
	    Matcher matcher = pattern.matcher(str);
	    char ch;
	    while (matcher.find()) {
	        ch = (char) Integer.parseInt(matcher.group(2), 16);
	        str = str.replace(matcher.group(1), ch + "");    
	    }
	    return str;
	}
	
	protected String sendPost(String url, String content) {
		
		HttpPost httpPost = new HttpPost(url);
		HttpEntity bodyEntity = new StringEntity(content, API_CHARSET);
		httpPost.setEntity(bodyEntity);
		
		try {
			HttpResponse response = httpClient.execute(httpPost);
		    HttpEntity entity = response.getEntity();
		    return EntityUtils.toString(entity);
		} catch (Exception e) {
			logger.error(e.getMessage(), e);
			throw new IllegalStateException(e);
		}
		
	}
	 
	 
}
